/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the AndroidAnnotations project
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed To in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package org.ohosannotations.internal;

import org.ohosannotations.Option;
import org.ohosannotations.helper.OhosManifest;
import org.ohosannotations.helper.ModelConstants;
import org.ohosannotations.internal.core.CorePlugin;
import org.ohosannotations.internal.exception.OhosManifestNotFoundException;
import org.ohosannotations.internal.exception.ProcessingException;
import org.ohosannotations.internal.exception.RClassNotFoundException;
import org.ohosannotations.internal.exception.VersionMismatchException;
import org.ohosannotations.internal.exception.VersionNotFoundException;
import org.ohosannotations.internal.generation.CodeModelGenerator;
import org.ohosannotations.internal.helper.OhosManifestFinder;
import org.ohosannotations.internal.helper.ErrorHelper;
import org.ohosannotations.internal.model.AnnotationElements;
import org.ohosannotations.internal.model.AnnotationElementsHolder;
import org.ohosannotations.internal.model.ModelExtractor;
import org.ohosannotations.internal.process.ModelProcessor;
import org.ohosannotations.internal.process.ModelValidator;
import org.ohosannotations.internal.process.TimeStats;
import org.ohosannotations.internal.rclass.OhosRclassFinder;
import org.ohosannotations.internal.rclass.CompoundRclass;
import org.ohosannotations.internal.rclass.ProjectRClassFinder;
import org.ohosannotations.logger.Level;
import org.ohosannotations.logger.Logger;
import org.ohosannotations.logger.LoggerContext;
import org.ohosannotations.logger.LoggerFactory;
import org.ohosannotations.plugin.OhosAnnotationsPlugin;
import org.ohosannotations.rclass.IRClass;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

/**
 * 嗳哟注解处理器
 *
 * @author dev
 * @since 2021-07-22
 */
public class OhosAnnotationProcessor extends AbstractProcessor {
    /**
     * 选择增量
     */
    public static final Option OPTION_INCREMENTAL = new Option("incremental", "false");

    private static final Logger LOGGER = LoggerFactory.getLogger(OhosAnnotationProcessor.class);

    private String coreVersion;

    private final TimeStats timeStats = new TimeStats();
    private final ErrorHelper errorHelper = new ErrorHelper();
    private InternalOhosAnnotationsEnvironment ohosAnnotationsEnv;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        ohosAnnotationsEnv = new InternalOhosAnnotationsEnvironment(processingEnv);

        ModelConstants.init(ohosAnnotationsEnv);

        // Configure Logger
        LoggerContext loggerContext = LoggerContext.getInstance();
        loggerContext.setEnvironment(ohosAnnotationsEnv);

        try {
            OhosAnnotationsPlugin corePlugin = new CorePlugin(); // 内置一些核心插件，
            corePlugin.loadVersion();
            coreVersion = corePlugin.getVersion();

            LOGGER.warn("Initialize OhosAnnotations {} with options {}", coreVersion, processingEnv.getOptions());

            List<OhosAnnotationsPlugin> plugins = loadPlugins(); // 查找全部实现了OhosAnnotationsPlugin接口的类并实例化
            plugins.add(0, corePlugin);
            ohosAnnotationsEnv.setPlugins(plugins);
        } catch (Exception e) {
            LOGGER.error(e, "Can't load plugins");
        }
    }

    private List<OhosAnnotationsPlugin> loadPlugins() throws FileNotFoundException, VersionNotFoundException {
        ServiceLoader<OhosAnnotationsPlugin> serviceLoader
            = ServiceLoader.load(OhosAnnotationsPlugin.class, OhosAnnotationsPlugin.class.getClassLoader());
        List<OhosAnnotationsPlugin> plugins = new ArrayList<>();
        for (OhosAnnotationsPlugin plugin : serviceLoader) {
            plugins.add(plugin);

            if (plugin.shouldCheckApiAndProcessorVersions()) {
                plugin.loadVersion();
            }
        }
        LOGGER.info("Plugins loaded: {}", Arrays.toString(plugins.toArray()));
        return plugins;
    }

    /**
     * 过程
     *
     * @param annotations 注释
     * @param roundEnv 一轮env
     * @return boolean
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        timeStats.clear();
        timeStats.start("Whole Processing");

        Set<? extends Element> rootElements = roundEnv.getRootElements(); // 返回上一轮注解处理器生成的根元素
        if (LOGGER.isLoggable(Level.TRACE)) {
            LOGGER.trace("Start processing for {} annotations {} on {} elements {}",
                annotations.size(), annotations, rootElements.size(), rootElements);
        } else {
            LOGGER.info("Start processing for {} annotations on {} elements",
                annotations.size(), rootElements.size());
        }

        try {
            checkApiAndProcessorVersions();
            processThrowing(annotations, roundEnv);
        } catch (ProcessingException e) {
            handleException(annotations, roundEnv, e);
        } catch (Exception e) {
            handleException(annotations, roundEnv, new ProcessingException(e, null));
        }
        timeStats.stop("Whole Processing");
        timeStats.logStats();

        LOGGER.warn("Finish processing");

        LoggerContext.getInstance().close(roundEnv.processingOver());
        return true;
    }

    private void checkApiAndProcessorVersions() throws VersionMismatchException {
        for (OhosAnnotationsPlugin plugin : ohosAnnotationsEnv.getPlugins()) {
            if (plugin.shouldCheckApiAndProcessorVersions() && !plugin.getApiVersion().equals(plugin.getVersion())) {
                LOGGER.error("{} version for API ({}) and processor ({}) " +
                    "don't match. Please check your classpath",
                    plugin.getName(), plugin.getApiVersion(), plugin.getVersion());
                throw new VersionMismatchException(
                    plugin.getName() + "version for API (" + plugin.getApiVersion()
                        + ") and core (" + plugin.getVersion() + ") don't match. Please check your classpath");
            }
        }
    }

    private void processThrowing(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) throws Exception {
        if (nothingToDo(annotations, roundEnv)) {
            return;
        }

        AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
        ohosAnnotationsEnv.setExtractedElements(extractedModel);

        AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
        ohosAnnotationsEnv.setValidatedElements(validatingHolder);

        try {
            OhosManifest ohosManifest = extractOhosManifest();
            LOGGER.info("OhosConfig.json found: {}", ohosManifest);

            IRClass rClass = findRClasses(ohosManifest);

            ohosAnnotationsEnv.setOhosEnvironment(rClass, ohosManifest);

        } catch (Exception e) {
            return;
        }

        AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);
        ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);

        generateSources(processResult);
    }

    private boolean nothingToDo(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv) {
        return roundEnv.processingOver() || annotations.size() == 0;
    }

    private AnnotationElementsHolder extractAnnotations
        (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        timeStats.start("Extract Annotations");
        ModelExtractor modelExtractor = new ModelExtractor();
        AnnotationElementsHolder extractedModel = modelExtractor
            .extract(annotations, getSupportedAnnotationTypes(), roundEnv);
        timeStats.stop("Extract Annotations");
        return extractedModel;
    }

    private OhosManifest extractOhosManifest() throws OhosManifestNotFoundException {
        try {
            timeStats.start("Extract Manifest");
            return new OhosManifestFinder(ohosAnnotationsEnv).extractOhosManifest();
        } finally {
            timeStats.stop("Extract Manifest");
        }
    }

    private IRClass findRClasses(OhosManifest ohosManifest) throws RClassNotFoundException {
        try {
            timeStats.start("Find R Classes");
            IRClass rClass = new ProjectRClassFinder(ohosAnnotationsEnv).find(ohosManifest);
            IRClass ohosRClass = new OhosRclassFinder(processingEnv).find();
            return new CompoundRclass(rClass, ohosRClass);
        } finally {
            timeStats.stop("Find R Classes");
        }
    }

    private AnnotationElements validateAnnotations(AnnotationElements extractedModel,
        AnnotationElementsHolder validatingHolder) {
        timeStats.start("Validate Annotations");
        ModelValidator modelValidator = new ModelValidator(ohosAnnotationsEnv);
        AnnotationElements validatedAnnotations = modelValidator
            .validate(extractedModel, validatingHolder);
        timeStats.stop("Validate Annotations");
        return validatedAnnotations;
    }

    private ModelProcessor.ProcessResult processAnnotations(AnnotationElements validatedModel) throws Exception {
        timeStats.start("Process Annotations");
        ModelProcessor modelProcessor = new ModelProcessor(ohosAnnotationsEnv);
        ModelProcessor.ProcessResult processResult = modelProcessor.process(validatedModel);
        timeStats.stop("Process Annotations");
        return processResult;
    }

    private void generateSources(ModelProcessor.ProcessResult processResult) throws IOException {
        timeStats.start("Generate Sources");
        LOGGER.info("Number of files generated by OhosAnnotations: {}"
            + "", processResult.codeModel.countArtifacts());
        CodeModelGenerator modelGenerator = new CodeModelGenerator(processingEnv.getFiler(),
            coreVersion, ohosAnnotationsEnv.getOptionValue(CodeModelGenerator.OPTION_ENCODING));
        modelGenerator.generate(processResult);
        timeStats.stop("Generate Sources");
    }

    private void handleException(Set<? extends TypeElement> annotations,
        RoundEnvironment roundEnv, ProcessingException e1) {
        String errorMessage = errorHelper.getErrorMessage(processingEnv, e1, coreVersion);

        /*
         * Printing exception as an error on a random element. The exception is not
         * related to this element, but otherwise it wouldn't show up in eclipse.
         */

        Iterator<? extends TypeElement> iterator = annotations.iterator();
        if (iterator.hasNext()) {
            Element element = roundEnv.getElementsAnnotatedWith(iterator.next()).iterator().next();
            LOGGER.error(element, "Something went wrong: {}", errorMessage);
        } else {
            LOGGER.error("Something went wrong: {}", errorMessage);
        }
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return ohosAnnotationsEnv.getSupportedAnnotationTypes();
    }

    @Override
    public Set<String> getSupportedOptions() {
        return ohosAnnotationsEnv.getSupportedOptions();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }
}
